package com.spzx.order.service.impl;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.cart.api.RemoteCartService;
import com.spzx.cart.domain.CartInfo;
import com.spzx.common.core.context.SecurityContextHolder;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.utils.DateUtils;
import com.spzx.common.core.utils.uuid.IdUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.core.web.domain.AjaxResult;
import com.spzx.common.rabbitmq.service.RabbitmqService;
import com.spzx.order.domain.OrderItem;
import com.spzx.order.domain.OrderLog;
import com.spzx.order.domain.dto.OrderSubmitDTO;
import com.spzx.order.domain.vo.OrderTradeVO;
import com.spzx.order.service.IOrderItemService;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.api.RemoteSkuStockService;
import com.spzx.product.domain.dto.SkuPriceDTO;
import com.spzx.product.domain.dto.SkuStockDTO;
import com.spzx.user.api.RemoteUserInfoService;
import com.spzx.user.domain.UserAddress;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.spzx.order.mapper.OrderInfoMapper;
import com.spzx.order.domain.OrderInfo;
import com.spzx.order.service.IOrderInfoService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

/**
 * 订单Service业务层处理
 *
 * @author atguigu
 * @date 2024-12-25
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements IOrderInfoService
{
    @Resource
    private OrderInfoMapper orderInfoMapper;
    @Resource
    RemoteCartService remoteCartService;
    @Resource
    private IOrderItemService orderItemService;
    @Resource
    RemoteUserInfoService remoteUserInfoService;
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    RemoteProductService remoteProductService;
    @Resource
    RemoteSkuStockService remoteSkuStockService;
    @Resource
    RabbitmqService rabbitmqService;

    /**
     * 查询订单列表
     *
     * @param orderInfo 订单
     * @return 订单
     */
    @Override
    public List<OrderInfo> selectOrderInfoList(OrderInfo orderInfo)
    {
        return orderInfoMapper.selectOrderInfoList(orderInfo);
    }
    //准备结算页需要的结算数据
    @Override
    public OrderTradeVO trade() {
        //1、查询当前用户购物车中选中的购物项
        Long userId = SecurityContextHolder.getUserId();
        //远程访问购物车服务查询该用户购物车中选中的购物项
        R<List<CartInfo>> listR = remoteCartService.getCartInfos(userId);
        if(listR.getCode()!=200 || CollectionUtils.isEmpty(listR.getData())){
            throw new ServiceException("没有选择要购买的商品");
        }
        List<CartInfo> cartInfos = listR.getData();

        OrderTradeVO tradeVO = new OrderTradeVO();
        //订单项集合： 由购物车中的选中的购物项转换得到
        List<OrderItem> orderItems = cartInfos.stream().map(cartInfo -> {
            OrderItem orderItem = new OrderItem();
            orderItem.setSkuId(cartInfo.getSkuId());//!!!!
            orderItem.setSkuNum(cartInfo.getSkuNum());
            orderItem.setSkuPrice(cartInfo.getSkuPrice());
            orderItem.setSkuName(cartInfo.getSkuName());
            orderItem.setThumbImg(cartInfo.getThumbImg());
            return orderItem;
        }).collect(Collectors.toList());
        tradeVO.setOrderItemList(orderItems); //orderItem订单项  和购物想 cartInfo是1：1的关系
        // 订单项商品总金额
        BigDecimal totalAmount = orderItems.stream()
                //计算每个订单项的小计金额：使用他的skuNum*skuPrice得到
                .map(orderItem -> orderItem.getSkuPrice()
                        .multiply(new BigDecimal(orderItem.getSkuNum().toString()))
                )
                //将上面的每两个小计金额累加
                .reduce((a, b) -> a.add(b))
                //获取累加的最终结果
                .get();
        tradeVO.setTotalAmount(totalAmount);
        // 订单编号
        tradeVO.setTradeNo(IdUtils.fastSimpleUUID());
        return tradeVO;
    }
    //提交订单：返回订单id     保存订单数据   拆分订单 和订单项数据分别保存到 orderInfo和orderItem表中
    // 一个订单包含多个订单项，他们是1：n的关系
    // 订单属于当前登录的用户，一个用户可以有多个订单
    //  订单和userId绑定 ， 订单项和订单id绑定
    @Override
    public Long submitOrder(OrderSubmitDTO orderSubmitDTO) {
        //直接保存数据到数据库的问题：
        // 1、幂等性问题 同一个页面提交的订单数据多次提交保存多次
        // trade接口 返回订单确认页数据时 设置了一个tradeNo 交易流水号
        //   从订单确认页 提交订单时 会进入当前业务，如果tradeNo相同我们认为请求来自于同一个订单确认页 只处理一次
        String tradeNo = orderSubmitDTO.getTradeNo();
        //使用setnx 以tradeNo作为key 存到redis中，设置失败 表示tradeNo已经被使用过 不能继续使用
        String tradeNoKey = "spzx:order:submit:"+tradeNo;
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(tradeNoKey, "1", 5, TimeUnit.MINUTES);
        if(!flag){
            //设置失败  表示同一个页面多次提交订单
            throw new ServiceException("订单重复提交！");
        }
        // 2、验证前端提交的订单项商品价格是否正确： 和数据库最新的价格对比
        //  可以将每个订单项的金额 提交给 spzx-product 进行比对，如果全部正确 才继续处理
        //  验价格： 可以将要验价的skuId、price、skuName 和 验价的状态  一起提交给spzx-product
        //      spzx-prodcut 根据skuId查询数据库商品最新价格和订单商品价格比较，比较成功验价状态为true
        List<OrderItem> orderItemList = orderSubmitDTO.getOrderItemList();
        if(CollectionUtils.isEmpty(orderItemList)){
            throw new ServiceException("没有选中要购买的商品!!");
        }
        //2.1 将订单项转为要验价的 验价对象
        List<SkuPriceDTO> priceDTOS = orderItemList.stream().map(orderItem -> {
            SkuPriceDTO skuPriceDTO = new SkuPriceDTO();
            skuPriceDTO.setSkuId(orderItem.getSkuId());
            skuPriceDTO.setSkuPrice(orderItem.getSkuPrice());
            skuPriceDTO.setSkuName(orderItem.getSkuName());
//            skuPriceDTO.setFlag();
            return skuPriceDTO;
        }).collect(Collectors.toList());
        //2.2 访问spzx-product的接口 对sku进行价格校验
        R<List<SkuPriceDTO>> skuPricesR = remoteProductService.checkSkuPrices(priceDTOS);
        if(skuPricesR.getCode()!=200){
            throw new ServiceException("商品价格校验失败!!");
        }
        //获取验价结果
        List<SkuPriceDTO> skuPrices = skuPricesR.getData();
        //获取验价失败的结果
        List<SkuPriceDTO> checkedFailPrices = skuPrices.stream()
                .filter(skuPriceDTO -> !skuPriceDTO.isFlag())//验价状态为false的表示失败
                .collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(checkedFailPrices)){
            String skuNames = checkedFailPrices.stream()
                    .map(SkuPriceDTO::getSkuName) //获取每一个失败价格信息的 sku标题
                    .collect(Collectors.joining(","));//对标题进行字符串拼接
            throw new ServiceException("商品【"+skuNames+"】价格校验失败!!");
        }
        // 当前行表示 价格校验成功了...

        // 3、库存超卖问题： 如果库存不足 会出现超卖   验库存锁库存
        /*
            1、不允许超卖： 电商平台
                1.1 允许适量的超卖
                    可以超卖 但是不能超的太多
                    > 提交订单时 判断库存是否足够，如果足够可以继续向后执行(不锁库存)
                    > 用户支付成功时，再去扣减库存
                        - 以后的用户再来提交订单 校验库存如果不足 不能提交订单
                        - 用户支付成功前，库存没有扣减 提交订单的用户都可以支付成功
                1.2 严格不允许超卖
                    > 数据库库存字段一般设置为 undesign类型(无符号 不能为负数)
                    > 提交订单时，验库存锁库存(库存足够锁定库存！)
                        库存表：stock
                            skuId   stock(库存)    lock_stock(锁定库存)    available_stock(可用库存)  sale_num(销量)
                            1        100                10                    80                   10

                        - 提交订单锁库存： available_stock-skuNum ,  lock_stock+skuNum
                        - 用户不支付取消订单 释放库存： lock_stock-skuNum , available_stock+skuNum
                        - 用户支付成功，扣减库存 增加销量： lock_stock-skuNum , sale_num+skuNum

            2、允许超卖：  虚拟商品  听书项目中的专辑！
         */
        String orderNo = UUID.fastUUID().toString(); //生成订单编号
        //3.1 封装要验库存的商品信息：远程访问spzx-product服务
        //  skuId , skuNum ,skuName , flag(true表示验库存锁库存成功) , orderNo(要验库存锁库存的信息属于哪个订单的！)
        List<SkuStockDTO> skuStockDTOS = orderItemList.stream()
                .map(orderItem -> {
                    SkuStockDTO skuStockDTO = new SkuStockDTO();
                    skuStockDTO.setSkuId(orderItem.getSkuId());
                    skuStockDTO.setSkuName(orderItem.getSkuName());
                    skuStockDTO.setSkuNum(orderItem.getSkuNum());
                    skuStockDTO.setOrderNo(orderNo);
                    return skuStockDTO;
                }).collect(Collectors.toList());
        //3.2 远程访问 验库存锁库存 并返回验库存锁库存的结果
        R<List<SkuStockDTO>> skuStocksR = remoteSkuStockService.checkAndLockSkuStock(skuStockDTOS);
        //3.3 库存校验/锁定库存失败抛出异常
        if(skuStocksR.getCode()!=200){
            throw new ServiceException("商品库存锁定失败!!");
        }
        //获取验库存锁库存的结果
        List<SkuStockDTO> stockDTOS = skuStocksR.getData();
        //获取验库存锁库存失败的集合
        List<SkuStockDTO> checkedFailStocks = stockDTOS.stream()
                .filter(skuStockDTO -> !skuStockDTO.isFlag()).collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(checkedFailStocks)){
            //存在锁库存失败的
            String skuNames = checkedFailStocks.stream()
                    .map(SkuStockDTO::getSkuName) //获取每一个失败库存信息的 sku标题
                    .collect(Collectors.joining(","));//对标题进行字符串拼接
            throw new ServiceException("商品【"+skuNames+"】库存不足!!");
        }
        //当前行表示：库存校验 锁定库存成功
        // 4、保存订单和订单项到数据库
        Long orderInfoId = saveOrderInfo(orderSubmitDTO,orderNo);

        // 5、延迟关单：如果用户一直不支付  为了及时释放库存！   当前位置如果4出现了异常不会执行，在锁定库存成功以后发送关单消息
        //   锁定库存后，如果用户30分钟不支付 希望订单修改为关闭状态  订单锁定的库存及时释放
        //    rabbitmq：延迟队列
        //    5.1 锁定库存成功 发送消息 到设置了过期时间(30分钟)的队列
        //    5.2 给上面的队列 绑定死信队列(延迟队列)  消息过期时会自动变为死信进入到死信队列(延迟队列)
        //    5.3 创建消费者 监听死信队列，就可以在30分钟后获取到消息消费

        // 6、清空购物车： 提交订单成功后，删除购物车中选中的购物项
        // 也可以通过rabbitmq实现：
        // 执行到当前行 表示订单创建成功 ，发送一个订单创建成功的消息(1、购物车服务监听 清空购物车 ， 2、订单服务监听  保存订单日志)
        Map<String,Object> map = new HashMap<>();//消息内容
        map.put("userId" , SecurityContextHolder.getUserId());
        map.put("orderId" , orderInfoId);
        map.put("orderStatus" , 0);
        //创建交换机队列绑定：配置类创建 或者 RabbitListener注解创建
        rabbitmqService.sendMsg("spzx.order.submmit.exchange",
                "spzx.order.submit.msg",map
                );

        // spzx-cart:消费订单创建成功的消息 清空购物车
        // spzx-order: 消费订单创建成功的消息 保存订单日志
        // spzx-order延迟关单 ： 修改订单的状态 发送释放库存的消息
        // spzx-product: 消费释放库存的消息  释放锁定的库存
        // 7、返回订单id
        return orderInfoId;
    }
    //提交订单后 完成支付！！！
    //aop只能作用于 public方法
    @Transactional(rollbackFor = Exception.class) //使用本地事务 保证一个业务中的多个sql操作 要么一起成功 要么一起失败回滚
    public Long saveOrderInfo(OrderSubmitDTO orderSubmitDTO,String orderNo) {
        List<OrderItem> orderItems = orderSubmitDTO.getOrderItemList();
        //计算总金额
        BigDecimal totalAmount = orderItems.stream()
                .map(orderItem -> orderItem.getSkuPrice()
                        .multiply(new BigDecimal(orderItem.getSkuNum().toString())))
                .reduce((a, b) -> a.add(b)).get();
        //保存订单数据：
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setUserId(SecurityContextHolder.getUserId());//订单绑定用户
        orderInfo.setNickName(SecurityContextHolder.getUserName()); //订单用户的名称
        orderInfo.setOrderNo(orderNo); //和订单id一样 都可以代表唯一的一个订单 ，展示给用户 或者和三方交互使用
        orderInfo.setCouponId(null); //用户使用的优惠券id
        orderInfo.setCouponAmount(null); //优惠金额
        orderInfo.setTotalAmount(totalAmount);//订单总金额: 根据订单项列表计算
        orderInfo.setOriginalTotalAmount(totalAmount); //订单原价：优惠前的价格
        orderInfo.setFeightFee(orderSubmitDTO.getFeightFee()); //运费
        orderInfo.setOrderStatus(0L); //订单状态： 0表示待付款
        //前端提交订单 只提交了地址id
        Long userAddressId = orderSubmitDTO.getUserAddressId();
        R<UserAddress> addressR = remoteUserInfoService.getAddress(userAddressId);
        if(addressR.getCode()!=200){
            throw new ServiceException("用户收货地址信息错误");
        }
        UserAddress userAddress = addressR.getData();
        orderInfo.setReceiverName(userAddress.getName());
        orderInfo.setReceiverAddress(userAddress.getFullAddress());
        orderInfo.setReceiverTagName(userAddress.getTagName());
        orderInfo.setReceiverPhone(userAddress.getPhone());
        orderInfo.setReceiverProvince(userAddress.getProvinceCode());
        orderInfo.setReceiverCity(userAddress.getCityCode());
        orderInfo.setReceiverDistrict(userAddress.getDistrictCode());

        this.save(orderInfo);
        //获取保存成功的订单的id
        Long orderInfoId = orderInfo.getId();

        //保存订单项列表时： 需要绑定订单id
        orderItems.forEach(orderItem -> {
            orderItem.setOrderId(orderInfoId);
            //skuId应该在 trade结算接口业务中 设置！！！！！
        });

        orderItemService.saveBatch(orderItems);
        return orderInfoId;
    }

}
